﻿using EnvDTE;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using VSmityukh.WP.TitleLocalizationTool.Common;

namespace VSmityukh.WP.TitleLocalizationTool
{
	[Guid("E5396879-5475-4166-A317-0DB5CBAB3A15")]
	public class TitleLocalizationTool
		: IVsSingleFileGenerator
	{
		public int DefaultExtension(out string defaultExtension)
		{
			defaultExtension = ".txt";
			return Microsoft.VisualStudio.VSConstants.S_OK;
		}

		public int Generate(string sourceFilePath, string sourceFileContent, string defaultNamespace, IntPtr[] outputFileContents, out uint output, IVsGeneratorProgress generateProgress)
		{
			var log = new StringBuilder();

			try
			{
				var dte = (EnvDTE.DTE)Package.GetGlobalService(typeof(EnvDTE.DTE));
				var sourceControl = dte.SourceControl;

				ProjectItem projectItem;

				try
				{
					projectItem = dte.Solution.FindProjectItem(sourceFilePath);
				}
				catch
				{
					log
						.Append("Error: There was an error while searching a ProjectItem associated to the [")
						.Append(sourceFilePath)
						.AppendLine("] file");

					throw;
				}

				if (projectItem == null)
				{
					throw new ApplicationException("Unable to locate a ProjectItem associated to the file");
				}

				projectItem.Properties.Item("BuildAction").Value = 0;

				var isWindowsPhone71 = IsWindowsPhone71Project(projectItem);

				var generatedFiles = new HashSet<string>();
				var localizationInfo = ParseSourceFile(sourceFileContent);

				if (localizationInfo.DefaultLanguageInfo == null)
				{
					throw new ApplicationException("Default language is not defined");
				}

				using (var sourceStream = typeof(TitleLocalizationTool).Assembly.GetManifestResourceStream("VSmityukh.WP.TitleLocalizationTool.Resources.AppResLib.dll"))
				{
					foreach (var languageInfo in localizationInfo.LanguageInfos)
					{
						if (!languageInfo.IsSupportedByWP7
							&& isWindowsPhone71)
						{
							continue;
						}

						var pathToTargetFile = Path.Combine(Path.GetDirectoryName(sourceFilePath), languageInfo.ResourceFileName);

						if (File.Exists(pathToTargetFile)
							&& sourceControl != null
							&& sourceControl.IsItemUnderSCC(pathToTargetFile)
							&& !sourceControl.IsItemCheckedOut(pathToTargetFile))
						{
							sourceControl.CheckOutItem(pathToTargetFile);
						}

						sourceStream.Position = 0;
						using (var targetStream = new FileStream(pathToTargetFile, FileMode.Create, FileAccess.Write, FileShare.None))
						{
							sourceStream.CopyTo(targetStream);
						}

						generatedFiles.Add(pathToTargetFile);
						UpdateResources(pathToTargetFile, languageInfo);						
					}
				}

				UpdateNestedProjectItems(projectItem, generatedFiles);
			}
			catch (Exception ex)
			{
				log
					.Append("Error: There was an error while processing the [")
					.Append(sourceFilePath)
					.AppendLine("] file")
					.Append("Error message: ")
					.AppendLine(ex.Message);

				return Microsoft.VisualStudio.VSConstants.S_FALSE;
			}
			finally
			{
				var header = Encoding.UTF8.GetPreamble();
				var content = Encoding.UTF8.GetBytes(log.ToString());

				var totalLenght = header.Length + content.Length;

				output = (uint)totalLenght;
				outputFileContents[0] = Marshal.AllocCoTaskMem(totalLenght);

				Marshal.Copy(header, 0, outputFileContents[0], header.Length);
				Marshal.Copy(content, 0, outputFileContents[0] + header.Length, content.Length);
			}

			return Microsoft.VisualStudio.VSConstants.S_OK;
		}

		private static void UpdateNestedProjectItems(ProjectItem projectItem, HashSet<string> generatedFiles)
		{
			foreach (ProjectItem item in projectItem.ProjectItems)
			{
				if (item.Properties != null)
				{
					var property = item.Properties.Item("FullPath");
					if (property != null)
					{
						var value = property.Value as string;
						if (value != null)
						{
							if (generatedFiles.Contains(value))
							{
								item.Properties.Item("BuildAction").Value = 2;
								generatedFiles.Remove(value);
							}
							else
							{
								if(value.EndsWith("txt", StringComparison.CurrentCultureIgnoreCase))
								{
									item.Properties.Item("BuildAction").Value = 0;
								}
								else
								{
									item.Remove();
								}
							}
						}
					}
				}
			}

			foreach (var file in generatedFiles)
			{
				var item = projectItem.ProjectItems.AddFromFile(file);
				item.Properties.Item("BuildAction").Value = 2;
			}
		}

		private static void UpdateResources(string pathToTargetFile, LanguageInfo languageInfo)
		{
			var hModule = APIHelper.BeginUpdateResource(pathToTargetFile, true);
			if (hModule == IntPtr.Zero)
			{
				throw new Win32Exception(Marshal.GetLastWin32Error());
			}

			var pointersToFree = new List<IntPtr>();

			try
			{
				var languageID = languageInfo.CultureInfo.LCID;

				var stringsTable = new StringsTable();

				stringsTable.Add(100, languageInfo.Title);
				stringsTable.Add(200, languageInfo.TileTitle);

				foreach (var stringBlock in stringsTable.GetBlocks())
				{
					var content = stringBlock.ToBytesArray();

					var stringBlockPtr = Marshal.AllocCoTaskMem(content.Length);
					pointersToFree.Add(stringBlockPtr);

					Marshal.Copy(content, 0, stringBlockPtr, content.Length);

					if (!APIHelper.UpdateResource(hModule, 6, stringBlock.BlockID, (ushort)languageID, stringBlockPtr, (uint)content.Length))
					{
						throw new Win32Exception(Marshal.GetLastWin32Error());
					}
				}

				if (!APIHelper.EndUpdateResource(hModule, false))
				{
					throw new Win32Exception(Marshal.GetLastWin32Error());
				}
			}
			catch
			{
				APIHelper.EndUpdateResource(hModule, true);
				throw;
			}
			finally
			{
				foreach (var pointer in pointersToFree)
				{
					Marshal.FreeCoTaskMem(pointer);
				}
			}
		}

		private LocalizationInfo ParseSourceFile(string sourceFileContent)
		{
			Guard.NotNull(sourceFileContent);

			var applicationName = "";
			var applicationDescription = "";
			var languageItems = new List<LanguageInfo>();

			if (!string.IsNullOrWhiteSpace(sourceFileContent))
			{
				var xml = XDocument.Parse(sourceFileContent);

				ValidateSourceFile(xml);

				var rootElement = xml.Root;

				var applicationElement = rootElement.Element("Application");
				if (applicationElement != null)
				{
					applicationName = applicationElement
						.Elements("Name")
						.Select(el => el.Value)
						.FirstOrDefault();

					applicationDescription = applicationElement
						.Elements("Description")
						.Select(el => el.Value)
						.FirstOrDefault();
				}

				languageItems = rootElement
					.Element("Languages")
					.Elements()
					.Where(el => el.Attribute("inactive").With(attr => !"true".Equals(attr.Value, StringComparison.OrdinalIgnoreCase), true))
					.Select(el => new LanguageInfo(el.Name.LocalName, el.Element("Title").Value, el.Element("TileTitle").Value))
					.ToList();
			}

			return new LocalizationInfo(applicationName, applicationDescription, languageItems);
		}

		private bool IsWindowsPhone71Project(ProjectItem projectItem)
		{
			Guard.NotNull(projectItem);

			try
			{
				var project = projectItem.ContainingProject;

				if (project != null
					&& project.Properties != null)
				{
					Property property = project.Properties.Item("WindowsPhoneProject.IsWindowsPhoneApplication");
					if (property != null
						&& (bool)property.Value)
					{
						property = project.Properties.Item("TargetFrameworkMoniker");

						return property != null
							&& (property.Value as string).With(val => val.IndexOf("WindowsPhone71", StringComparison.InvariantCultureIgnoreCase) >= 0);
					}
				}
			}
			catch { }

			return false;
		}

		private void ValidateSourceFile(XDocument sourceDocument)
		{
			Guard.NotNull(sourceDocument);

			using (var schemaStream = typeof(TitleLocalizationTool).Assembly.GetManifestResourceStream("VSmityukh.WP.TitleLocalizationTool.Resources.Localization.xsd"))
			using (var xmlReader = XmlReader.Create(schemaStream))
			{
				var schemaSet = new XmlSchemaSet();
				schemaSet.Add(null, xmlReader);

				var errors = new List<string>();
				sourceDocument.Validate(schemaSet, (o, e) => errors.Add(e.Message));

				if (errors.Any())
				{
					throw new ApplicationException("Source file is invalid: " + Environment.NewLine + string.Join(Environment.NewLine, errors));
				}
			}
		}
	}
}
